package com.jzo2o.market.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
import com.jzo2o.common.constants.ErrorInfo;
import com.jzo2o.common.expcetions.CommonException;
import com.jzo2o.common.expcetions.DBException;
import com.jzo2o.common.expcetions.ForbiddenOperationException;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.common.utils.BeanUtils;
import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.common.utils.JsonUtils;
import com.jzo2o.market.constants.RedisConstants;
import com.jzo2o.market.enums.ActivityStatusEnum;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.enums.CouponTypeEnum;
import com.jzo2o.market.mapper.ActivityMapper;
import com.jzo2o.market.model.domain.Activity;
import com.jzo2o.market.model.domain.Coupon;
import com.jzo2o.market.model.dto.request.ActivityPageQueryDTO;
import com.jzo2o.market.model.dto.request.ActivitySaveReqDTO;
import com.jzo2o.market.model.dto.request.SeizeCouponReqDTO;
import com.jzo2o.market.model.dto.response.ActivityInfoResDTO;
import com.jzo2o.market.model.dto.response.SeizeCouponInfoResDTO;
import com.jzo2o.market.service.IActivityService;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.mvc.utils.UserContext;
import com.jzo2o.mysql.utils.PageUtils;
import com.jzo2o.redis.properties.RedisSyncProperties;
import com.jzo2o.redis.utils.RedisSyncQueueUtils;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.jzo2o.market.constants.RedisConstants.RedisKey.*;

/**
 * <p>
 * 服务实现类
 * </p>
 * @author itcast
 * @since 2023-09-16
 */
@Service
public class ActivityServiceImpl extends ServiceImpl<ActivityMapper, Activity> implements IActivityService {
    @Resource
    private ICouponService couponService;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource(name = "seizeCouponScript")
    private RedisScript<Integer> seizeCouponScript;
    @Resource
    private RedisSyncProperties redisSyncProperties;

    @Override
    @SuppressWarnings("unchecked")
    public PageResult<ActivityInfoResDTO> page(ActivityPageQueryDTO activityPageQueryDTO) {
        // 分页查询
        Page<Activity> activityPage = PageUtils.parsePageQuery(activityPageQueryDTO, Activity.class);
        LambdaQueryWrapper<Activity> queryWrapper = Wrappers.<Activity>lambdaQuery()
                .eq(ObjectUtils.isNotEmpty(activityPageQueryDTO.getId()), Activity::getId, activityPageQueryDTO.getId())
                .like(ObjectUtils.isNotEmpty(activityPageQueryDTO.getName()), Activity::getName, activityPageQueryDTO.getName())
                .eq(ObjectUtils.isNotEmpty(activityPageQueryDTO.getType()), Activity::getType, activityPageQueryDTO.getType())
                .eq(ObjectUtils.isNotEmpty(activityPageQueryDTO.getStatus()), Activity::getStatus, activityPageQueryDTO.getStatus())
                // 新创建的活动显示在列表前面
                .orderByDesc(Activity::getCreateTime);
        Page<Activity> pageAns = baseMapper.selectPage(activityPage, queryWrapper);

        return PageUtils.toPage(pageAns, ActivityInfoResDTO.class);
    }

    @Override
    public ActivityInfoResDTO getDetailById(Long id) {
        Activity activity = baseMapper.selectById(id);
        if (ObjectUtils.isEmpty(activity)) {
            return new ActivityInfoResDTO();
        }

        ActivityInfoResDTO activityInfoResDTO = BeanUtils.toBean(activity, ActivityInfoResDTO.class);

        List<Coupon> coupons = couponService.lambdaQuery()
                .eq(Coupon::getActivityId, id)
                .select(Coupon::getStatus)
                .list();

        activityInfoResDTO.setReceiveNum(coupons.size());
        activityInfoResDTO.setWriteOffNum((int) coupons.stream()
                .filter(coupon -> coupon.getStatus() == CouponStatusEnum.USED)
                .count());

        return activityInfoResDTO;
    }

    @Override
    public List<SeizeCouponInfoResDTO> getCachedActivity(ActivityStatusEnum status) {
        List<SeizeCouponInfoResDTO> couponInfoList = JsonUtils.toList((String) redisTemplate.opsForValue()
                .get(ACTIVITY_CACHE_LIST), SeizeCouponInfoResDTO.class);

        if (CollUtils.isEmpty(couponInfoList)) {
            return new ArrayList<>();
        }

        LocalDateTime nowTime = LocalDateTime.now();
        return couponInfoList.stream()
                .peek(coupon -> {
                    // 防止缓存中的状态出错
                    if (coupon.getDistributeEndTime().isBefore(nowTime)) {
                        coupon.setStatus(ActivityStatusEnum.LOSE_EFFICACY);
                    } else if (coupon.getDistributeStartTime().isBefore(nowTime)) {
                        coupon.setStatus(ActivityStatusEnum.DISTRIBUTING);
                    }
                })
                .filter(coupon -> coupon.getStatus() == status)
                .collect(Collectors.toList());
    }

    @Override
    @Transactional
    public void saveOrUpdate(ActivitySaveReqDTO activitySaveReqDTO) {
        // 参数检验
        activitySaveReqDTO.check();

        Activity activity = BeanUtils.toBean(activitySaveReqDTO, Activity.class);
        activity.setType(CouponTypeEnum.typeOf(activitySaveReqDTO.getType()));

        if (ObjectUtils.isEmpty(activity.getId())) {
            // 新增记录初始化相关状态
            activity.setStatus(ActivityStatusEnum.NO_DISTRIBUTE)
                    .setStockNum(activity.getTotalNum());
        } else {
            // 更新需要检查活动状态
            Activity activityInDb = baseMapper.selectById(activity.getId());
            if (ObjectUtils.isEmpty(activityInDb)) {
                throw new ForbiddenOperationException("活动不存在无法修改");
            } else if (activityInDb.getStatus() != ActivityStatusEnum.NO_DISTRIBUTE) {
                throw new ForbiddenOperationException("活动状态错误无法修改");
            }
        }

        if (!this.saveOrUpdate(activity)) {
            throw new DBException("新增/更新活动信息失败");
        }
    }

    @Override
    @Transactional
    public void revoke(Long id) {
        Activity activityInDb = baseMapper.selectById(id);
        if (ObjectUtils.isEmpty(activityInDb)) {
            throw new ForbiddenOperationException("活动不存在无法撤销");
        } else if (activityInDb.getStatus() != ActivityStatusEnum.NO_DISTRIBUTE
                && activityInDb.getStatus() != ActivityStatusEnum.DISTRIBUTING) {
            throw new ForbiddenOperationException("活动状态错误无法撤销");
        }

        if (!lambdaUpdate()
                .eq(Activity::getId, id)
                .set(Activity::getStatus, ActivityStatusEnum.VOIDED)
                .update()) {
            throw new DBException("更新活动表失败");
        }

        // 优惠卷可能没有发放 -> 更新0行
        ChainWrappers.lambdaUpdateChain(couponService.getBaseMapper())
                .eq(Coupon::getActivityId, id)
                .set(Coupon::getStatus, CouponStatusEnum.VOIDED)
                .update();
    }

    @Override
    public void updateActivityStatus() {
        LocalDateTime nowTime = LocalDateTime.now();
        List<Activity> activityList = lambdaQuery()
                .in(Activity::getStatus, ActivityStatusEnum.NO_DISTRIBUTE, ActivityStatusEnum.DISTRIBUTING)
                // 不处理还没有发生的活动
                .le(Activity::getDistributeStartTime, nowTime)
                .list();

        if (CollUtils.isEmpty(activityList)) {
            return;
        }

        for (Activity activity : activityList) {
            ActivityStatusEnum status;
            if (activity.getDistributeEndTime().isBefore(nowTime)) {
                // 活动结束
                status = ActivityStatusEnum.LOSE_EFFICACY;
            } else if (activity.getDistributeStartTime().isBefore(nowTime)) {
                // 活动开始
                status = ActivityStatusEnum.DISTRIBUTING;
            } else {
                continue;
            }

            if (!lambdaUpdate()
                    .eq(Activity::getId, activity.getId())
                    .set(Activity::getStatus, status)
                    .update()) {
                throw new DBException("更新活动状态失败");
            }
        }
    }

    @Override
    public void cacheComingActivity() {
        LocalDateTime nowTime = LocalDateTime.now();

        // 获取近一个月未开始/已开始的活动
        @SuppressWarnings("unchecked")
        List<Activity> activityList = lambdaQuery()
                .le(Activity::getDistributeStartTime, nowTime.plusDays(30))
                .in(Activity::getStatus, Arrays.asList(ActivityStatusEnum.NO_DISTRIBUTE, ActivityStatusEnum.DISTRIBUTING))
                .orderByAsc(Activity::getDistributeStartTime)
                .list();

        if (CollUtils.isEmpty(activityList)) {
            activityList = new ArrayList<>();
        }

        List<SeizeCouponInfoResDTO> couponInfoList = activityList.stream()
                .map(activity -> BeanUtils
                        .toBean(activity, SeizeCouponInfoResDTO.class)
                        .setRemainNum(activity.getStockNum())
                        .setType(activity.getType().getType()))
                .collect(Collectors.toList());

        // 缓存活动列表
        redisTemplate.opsForValue().set(RedisConstants.RedisKey.ACTIVITY_CACHE_LIST, JsonUtils.toJsonStr(couponInfoList));

        // 缓存优惠卷库存
        HashOperations<String, Object, Object> hashOperations = redisTemplate.opsForHash();
        couponInfoList.forEach(coupon -> {
            String key = String.format(RedisConstants.RedisKey.COUPON_RESOURCE_STOCK, coupon.getId() % redisSyncProperties.getQueueNum());
            // 防止进行中的活动的库存被覆盖
            if (coupon.getStatus() == ActivityStatusEnum.NO_DISTRIBUTE) {
                hashOperations.put(key, coupon.getId(), coupon.getRemainNum());
            } else {
                hashOperations.putIfAbsent(key, coupon.getId(), coupon.getRemainNum());
            }
        });
    }

    @Override
    public void seizeCoupon(SeizeCouponReqDTO seizeCouponReqDTO) {
        Long activityId = seizeCouponReqDTO.getId();
        Activity activity = lambdaQuery()
                .eq(Activity::getId, activityId)
                .select(Activity::getDistributeStartTime, Activity::getDistributeEndTime)
                .one();

        int seizeCouponFailedCode = ErrorInfo.Code.SEIZE_COUPON_FAILD;
        if (ObjectUtils.isEmpty(activity)) {
            throw new CommonException(seizeCouponFailedCode, "活动不存在无法抢卷");
        } else if (activity.getDistributeStartTime().isAfter(LocalDateTime.now())) {
            throw new CommonException(seizeCouponFailedCode, "活动未开始无法抢卷");
        } else if (activity.getDistributeEndTime().isBefore(LocalDateTime.now())) {
            throw new CommonException(seizeCouponFailedCode, "活动已结束无法抢卷");
        }

        Long userId = UserContext.currentUserId();
        if (ObjectUtils.isEmpty(userId)) {
            throw new CommonException(seizeCouponFailedCode, "用户信息不存在无法抢卷");
        }

        int activityTag = (int) (activityId % redisSyncProperties.getQueueNum());
        String syncQueueKey = RedisSyncQueueUtils.getQueueRedisKey(COUPON_SEIZE_SYNC_QUEUE_NAME, activityTag);
        String stockTable = String.format(COUPON_RESOURCE_STOCK, activityTag);
        String seizeSuccessList = String.format(COUPON_SEIZE_LIST, activityId, activityTag);
        List<String> keys = Arrays.asList(syncQueueKey, stockTable, seizeSuccessList);

        // 调用抢卷脚本
        int state = Optional
                .ofNullable(redisTemplate.execute(seizeCouponScript, keys, activityId, userId))
                .orElseThrow(() -> new CommonException(seizeCouponFailedCode, "抢卷失败"));

        if (state < 0) {
            String failMag = state == -1 ? "请勿重复抢卷" :
                    (state == -2 || state == -4 ? "库存不足抢卷失败" : "内部错误抢卷失败");
            throw new CommonException(seizeCouponFailedCode, failMag);
        }
    }
}